#!/usr/bin/python

# Tor control port filter proxy, only white-listing SIGNAL NEWNYM.

# This filter proxy should allow Torbutton to request a
# new Tor circuit, without exposing dangerous control requests
# like "GETINFO address" to applications running as a local user.

# If something goes wrong, an error code is returned, and
# Torbutton will display a warning dialog that New Identity failed.

# Only one application can talk through this filter proxy
# simultaneously. A malicious application that is running as a
# local user could use this to prevent other applications from
# doing NEWNYM. But it could just as well rewrite the
# TOR_CONTROL_PORT environment variable to itself or do something else.

import socket
import binascii
import re

# Limit the length of a line, to prevent DoS attacks trying to
# crash this filter proxy by sending infinitely long lines.
MAX_LINESIZE = 128

class UnexpectedAnswer(Exception):
	def __init__(self, msg):
		self.msg = msg
	def __str__(self):
		return "[UnexpectedAnswer] " + self.msg

def do_newnym_real():
	# Read authentication cookie
	with open("/var/run/tor/control.authcookie", "rb") as f:
		rawcookie = f.read(32)

	hexcookie = binascii.hexlify(rawcookie)

	# Connect to the real control port
	sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
	sock.settimeout(10.0)
	sock.connect("/var/run/tor/control")
	readh = sock.makefile("r")
	writeh = sock.makefile("w")

	# Authenticate
	writeh.write("AUTHENTICATE " + hexcookie + "\n")
	writeh.flush()

	answer = readh.readline(MAX_LINESIZE)
	if not answer.startswith("250"):
		raise UnexpectedAnswer("AUTHENTICATE failed")

	# Send the newnym signal
	writeh.write("SIGNAL NEWNYM\n")
	writeh.flush()

	answer = readh.readline(MAX_LINESIZE)
	if not answer.startswith("250"):
		raise UnexpectedAnswer("SIGNAL NEWNYM failed")

	# Close the connection
	writeh.write("QUIT\n")
	writeh.flush()

	answer = readh.readline(MAX_LINESIZE)
	if not answer.startswith("250"):
		raise UnexpectedAnswer("QUIT failed")

	sock.close()

def do_newnym():
	# Catch innocent exceptions, will report error instead
	try:
		do_newnym_real()
		print "Newnym went fine"
		return True
	except (IOError, UnexpectedAnswer) as e:
		print "Warning: Couldn't perform newnym!"
		print e
		return False

def handle_connection(sock):
	# Create file handles for the socket
	readh = sock.makefile("r")
	writeh = sock.makefile("w")

	# Keep accepting commands
	while True:
		# Read in a newline terminated line
		line = readh.readline(MAX_LINESIZE)
		if not line: break

                def line_matches_command(cmd):
                        # The control port language does not care about case
                        # for commands.
                        return re.match(r"^%s\b" % cmd, line, re.IGNORECASE)

		# Check what it is
		if line_matches_command("AUTHENTICATE"):
			# Don't check authentication, since only
			# safe commands are allowed
			writeh.write("250 OK\n")
		elif line_matches_command("SIGNAL NEWNYM"):
			# Perform a real SIGNAL NEWNYM (new Tor circuit)
			if do_newnym():
				writeh.write("250 OK\n")
			else:
				writeh.write("510 Newnym signal failed\n")
		elif line_matches_command("QUIT"):
			# Quit session
			writeh.write("250 Closing connection\n")
			break
		else:
			# Everything else we ignore/block
			writeh.write("510 Command filtered\n")

		# Ensure the answer was written
		writeh.flush()

	# Ensure all data was written
	writeh.flush()

def main():
	# Listen on port 9052 (we cannot use 9051 as Tor uses that one)
	server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
	server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
	server.bind(("127.0.0.1", 9052))
	server.listen(4)

	print "Tor control port filter started, listening on 9052"

	# Accept and handle connections one after one,
	# sessions are short enough that the added complexity of
	# simultaneous connections are unnecessary (in absence of attacks)
	while True:
		clisock, cliaddr = server.accept()

		try:
			print "Accepted a connection"
			handle_connection(clisock)
			print "Connection closed"
		except IOError:
			print "Connection closed (IOError)"

		clisock.close()

if __name__ == "__main__":
	main()
